The AzureServiceTokenProvider class from the Nuget package Microsoft.Azure.Services.AppAuthentication can be used to obtain an access token.  When running in Azure it can also utilize managed identities to request an access token.  In this post I’ll focus on using this class to get an access token for Azure Key Vault.  Keep in mind that you can also use this class to obtain an access token for any Azure resources integrated with Azure Active Directory.  Below are some code sample showing a couple of ways to use this class to get an access token and call Azure Key Vault:

Note: Before trying out the following, you should make sure the signed-in principal (application or user) has access to Azure Key Vault.  See my previous blog post for more info on this and the Azure Key Vault documentation.  For example, I have configured my Azure Key Vault to allow access to the following 2 users and 2 Applications in the Azure Key Vault’s Access Policies blade:

using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;
// ...
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
var secret = await kv.GetSecretAsync("https://blogkv123.vault.azure.net", "SQLPassword").ConfigureAwait(false);

or

using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.KeyVault;
// ...
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://vault.azure.net");
using (var client = new HttpClient())
{
    var url = " https://blogkv123.vault.azure.net/secrets/SQLPassword";
    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
    var response = await client.GetAsync(url);
    // Parse JSON response.
    ....
}

In the above code sample, we are using the default constructor of the AzureServiceTokenProvider class by instantiating an instance of this class without providing a connection string (which usually contains tenant and application ID info).  Later on, we will talk about how use a connection string with this class.  The default constructor tries to get an access token from the following methods:

  1. Managed Identities using client credentials grant flow – this option only works if the code is run in Azure environment, for example Azure Virtual Machine , Azure App Service, etc…

Note:  this option requires that the Azure resource has support for Managed Identities and Managed identity is enabled. For a list of Azure resources that support managed identities see https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/services-support-managed-identities

  1. Visual Studio
  2. Azure CLI or Active Directory Integrated Authentication

If all 3 methods fail, you can get an Exception like the following:

Exception thrown: ‘Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProviderException’ in System.Private.CoreLib.dll

Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/<tenant ID>. Exception Message: Tried the following 3 methods to get an access token, but none of them worked.

Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/<tenant ID>. Exception Message: Tried to get token using Managed Service Identity. Unable to connect to the Managed Service Identity (MSI) endpoint. Please check that you are running on an Azure resource that has MSI setup.

Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/<tenant ID>. Exception Message: Tried to get token using Visual Studio. Access token could not be acquired.

Parameters: Connection String: [No connection string specified], Resource: https://vault.azure.net, Authority: https://login.windows.net/<tenant ID>. Exception Message: Tried to get token using Azure CLI. Access token could not be acquired. ERROR: No subscription found. Run ‘az account set’ to select a subscription.

For option 1, either System MSI or User-Assigned MSI can be used.  Refer to the appropriate Azure Resource documentation for how to do this.  As mentioned above, remember to give MSI access to Azure Key Vault.

Below are a couple of screen shots showing how I configured my Azure VM MSI using either System MSI or User-Assigned MSI (see more info about User MSI Connection String later on in this blog):

A good way to test if MSI is enabled for the Azure VM is by running the following command from the Powershell window in the VM:

Invoke-WebRequest -Uri ‘http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net’ -Headers @{Metadata=”true”}

For Azure App Service, the MSI setting is similar in the Azure App Service’s Identity blade:

To test for MSI in Azure App Service you can create a file called ‘test.ps1’ with the following Powershell code and run the file from the Azure Web App’s kudu console:

$resourceURI = "https://vault.azure.net"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=2017-09-01"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
Echo $accessToken

And you should see output like the following:

For option 2, refer to the documentation at https://docs.microsoft.com/en-us/azure/key-vault/service-to-service-authentication for instruction under “Authentication with Visual Studio” section,

For option 3, make sure you have Azure CLI v2.0.12 or later installed (to find out what version you have installed run az –version command)

You will need to sign in from the az cli window first before running your application code.  You can either sign in by a user account or by an application (service principal) account

Sign in with a user account:

  1. Run az login to sign in
  2. Run az account get-access-token –resource https://vault.azure.net to verify you can get an access token for Azure Key Vault

Sign in with an Application (or Service Principal):

az login –service-principal -u <app id> –password <app secret> –tenant <tenant-id> –allow-no-subscriptions

Using AzureServiceTokenProvider with your own Application…

So far, we have only discussed calling AzureServiceTokenProvider class with no argument (the default constructor).  When deploying the code to run in Azure service, it mainly relies on using Managed Identity (MSI) configured for that service to get an access token.  Both the Azure CLI and Visual Studio method are intended only for local development effort so they are not suitable for production environment.  The problem with using MSI is that the Azure resource must support MSI and not every resource has this capability. 

Besides using Managed Identities, you can also configure your own Application Registration and use that application for the AzureServiceTokenProvider class to acquire an access token.  This can be accomplished using a Connection String to specify our tenant and application info detail.  See code example below for how to do this:

var azureServiceTokenProvider1 = new AzureServiceTokenProvider(“RunAs=App;AppId=<Application ID>;TenantId=<Tenant Name>.onmicrosoft.com;AppKey=<App Secret>”);

Note: For User-Assigned Managed Identity, use the connection string as followed:

var azureServiceTokenProvider1 = new AzureServiceTokenProvider(“RunAs=App;AppId=<User-Assigned MSI Application ID>”);

Set up the Connection String via an environment variable:

Instead of passing in the connection string in the AzureServiceTokenProvider class, you can create an environment variable (via Visual Studio Project Properties) called AzureServicesAuthConnectionString and set the connection string as its value:

Once this environment variable is set, you can call the AzureServiceTokenProvider with its default constructor:

var azureServiceTokenProvider = new AzureServiceTokenProvider();

For debugging purpose, I use the following code in my controller to get more info about the token or Exception Info:

public async System.Threading.Tasks.Task<ActionResult> Privacy()
        {
            // Instantiate a new KeyVaultClient object, with an access token to Key Vault
            var azureServiceTokenProvider1 = new AzureServiceTokenProvider();
             var azureServiceTokenProvider2 = new AzureServiceTokenProvider("RunAs=App;AppId=<Application ID>");
            // var azureServiceTokenProvider2 = new AzureServiceTokenProvider();
            // string accessToken = await azureServiceTokenProvider2.GetAccessTokenAsync("https://management.azure.com/").ConfigureAwait(false);
            // var azureServiceTokenProvider1 = new AzureServiceTokenProvider("RunAs=App;AppId=<Application ID>;TenantId=<Tenant Name>.onmicrosoft.com;AppKey=<Application Secret>");
            try
            {
                var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider1.KeyVaultTokenCallback));
                var secret = await kv.GetSecretAsync("https://blogkv123.vault.azure.net", "SQLPassword").ConfigureAwait(false);
                ViewBag.Secret = $"Secret: {secret.Value}";
            }
            catch (Exception ex)
            {
                ViewBag.Error = $"Something went wrong: {ex.Message}";
                Debug.WriteLine(ex.Message);
            }
            ViewBag.Principal = azureServiceTokenProvider1.PrincipalUsed != null ? $"Principal Used: {azureServiceTokenProvider1.PrincipalUsed}" : string.Empty;

            try
            {
                string accessToken = await azureServiceTokenProvider2.GetAccessTokenAsync("https://vault.azure.net").ConfigureAwait(false);
                ViewBag.AccessToken = $"Access Token: {accessToken}";
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
            }
            // Debug.WriteLine(ToString());
            return View();
        }

And here is the code for the View Page:

@{
    ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<div>

    <div>@ViewBag.AccessToken</div>

    <div>@ViewBag.Secret</div>

    <div>@ViewBag.Principal</div>

    <div>@ViewBag.Error</div>



</div>

<p>Use this page to detail your site's privacy policy.</p>

Beware of Token Lifetime

Usually Managed Identities (System or User-Assigned) access token is valid up to 8 hours. Make sure your application code checks for token validity and renewing the token as needed to avoid unpredictable behavior. A typical problem that can occur is intermittent Database connectivity issue, for instance the one documented here. An Azure App Service using Managed Identity access token to connect to an Azure SQL Database with the following Entity Framework code:

public class MyDbContext : DbContext
    {
        public MyDbContext(DbContextOptions options) : base(options)
        {
            var conn =  (SqlConnection) this.Database.GetDbConnection();
            conn.AccessToken = (new AzureServiceTokenProvider()).GetAccessTokenAsync("https://database.windows.net/").Result;
        }
        public DbSet<Item> Items { get; set; }
    }

//Startup.cs
services.AddDbContext<ToDoDbContext>(options =>
{
    var connStr = Configuration["ConnectionString"];
    options.UseSqlServer(connStr);
});

The code works fine initially but after some time it starts throwing the following exception:

System.Data.SqlClient.SqlException (0x80131904): Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'.
   at System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString connectionOptions, SqlCredential credential, Object providerInfo, String newPassword, SecureString newSecurePassword, Boolean redirectedUserInstance, SqlConnectionString userConnectionOptions, SessionData reconnectSessionData, Boolean applyTransientFaultHandling, String accessToken)
   at System.Data.SqlClient.SqlConnectionFactory.CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, Object poolGroupProviderInfo, DbConnectionPool pool, DbConnection owningConnection, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(DbConnectionPool pool, DbConnection owningObject, DbConnectionOptions options, DbConnectionPoolKey poolKey, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionPool.CreateObject(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
   at System.Data.ProviderBase.DbConnectionPool.UserCreateRequest(DbConnection owningObject, DbConnectionOptions userOptions, DbConnectionInternal oldConnection)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.ProviderBase.DbConnectionClosed.TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.Open()
   at Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnection(Boolean errorsExpected)
ClientConnectionId:a3a93762-2a94-473b-ba4b-9c3da55d669f
Error Number:18456,State:1,Class:14

The root cause of the above error is due to expired token being used. The Application code above only retrieves the access token once during DB Context initialization and keeps using the same token beyond its lifetime. Check out the github issue above (https://github.com/dotnet/efcore/issues/19808) for ideas on how to work around this issue.

References:

https://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient

https://www.rahulpnath.com/blog/authenticating-with-azure-key-vault-using-managed-service-identity/

https://docs.microsoft.com/en-us/samples/azure-samples/app-service-msi-keyvault-dotnet/keyvault-msi-appservice-sample/

https://docs.microsoft.com/en-us/samples/azure-samples/app-service-msi-keyvault-dotnet/keyvault-msi-appservice-sample/

https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity?context=azure%2Factive-directory%2Fmanaged-identities-azure-resources%2Fcontext%2Fmsi-context&tabs=powershell

https://docs.microsoft.com/en-us/azure/key-vault/service-to-service-authentication


Update 4/25/2020 – Added information about token life time and SQL Connection error

Leave a Comment